/*
 * BlackBox.java
 *
 * Created on March 10, 2008, 6:50 PM
 *
 * Copyright (C) 2007-2008  Hylke van der Schaaf
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, in version 3 of the License.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */
package hextd.blackBox;

import hextd.HexTD;
import hextd.hex.Hex;
import hextd.tower.Tower;
import hextd.tower.TowerFactory;
import hextd.tower.TowerHome;
import hextd.core.Context;
import hextd.core.ContextPlayer;
import hextd.message.GameMessage;
import hextd.message.Message;
import hextd.message.MessageBuyCreep;
import hextd.message.MessageFactory;
import hextd.message.MessageGameEnd;
import hextd.message.MessageLoadMap;
import hextd.message.MessagePlayerForfeit;
import hextd.message.MessagePlayerList;
import hextd.message.MessagePlayerList.MPPlayerInfo;
import hextd.message.MessageSetMode;
import hextd.message.MessageTick;
import hextd.message.MessageTowerBuy;
import hextd.message.MessageTowerSell;
import hextd.message.MessageTowerUpgrade;
import hextd.core.MapInfo;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.StringReader;
import java.util.ArrayList;
import java.util.List;

/**
 *
 * @author hylke
 */
public class BlackBox {

	private Context context;
	private final Context.GameTime gameTime;
	private List<BBAction> actions;
	private int replayPointer = 0;
	private List<BBActionListener> actionListeners;
	private List<GameMessage> backlog;

	public static enum ActionType {

		MapStart('L'),
		End('E'),
		NextWave('W'),
		NewTower('N'),
		UpgradeTower('U'),
		SellTower('S'),
		SetMode('M'),
		BuyCreeps('B'),
		PlayerList('P'),
		PlayerForfeit('F');
		public final char key;

		ActionType(char key) {
			this.key = key;
		}
	}

	/** Creates a new instance of BlackBox */
	public BlackBox(Context c) {
		this.context = c;
		gameTime = c.getGameTime();
		this.actions = new ArrayList<BBAction>();
		this.backlog = new ArrayList<GameMessage>();
		this.actionListeners = new ArrayList<BBActionListener>();
	}

	public void clear() {
		synchronized (gameTime) {
			this.actions.clear();
			switch (context.getGameState()) {
				case checking:
				case loadReplay:
				case replaying:
				case spPlaying:
				case spSelecting:
					this.backlog.clear();
					break;
			}
			this.replayPointer = 0;
		}
	}

	public void clearBacklog() {
		this.backlog.clear();
	}

	public BlackBox copy() {
		BlackBox tempBox = new BlackBox(this.context);
		BBAction[] actionArray = new BBAction[this.actions.size()];
		tempBox.actions.addAll(this.actions);
		return tempBox;
	}

	public void copyFrom(BlackBox source) {
		synchronized (gameTime) {
			this.clear();
			List<BBAction> sourceActions = source.getActions();
			this.actions.addAll(sourceActions);
		}
		this.fireActionCountChanged();
	}

	public void rewind() {
		this.replayPointer = 0;
	}

	public void endRecording() {
		synchronized (gameTime) {
			BBAction newAction = new BBActionEnd(gameTime.getTime(), context.getLocalPlayerId());
			this.actions.add(newAction);
		}
	}

	public int getNextActionTime() {
		if (this.replayPointer < this.actions.size()) {
			return this.actions.get(this.replayPointer).getGameTime();
		}
		return -1;
	}

	/**
	 * Prints the log that was recorded.
	 * @return StringBuilder containing the log.
	 */
	public StringBuilder printLog() {
		StringBuilder log = new StringBuilder();
		for (BBAction a : actions) {
			log.append(a.getString());
			log.append("\n");
		}
		return log;
	}

	public List<BBAction> getActions() {
		return actions;
	}

	public int getActionCount() {
		return this.actions.size();
	}

	public int getActionPointer() {
		return this.replayPointer + 1;
	}

	public int getNextWaveActionCount() {
		int count = 0;
		for (int i = this.actions.size() - 1; i >= 0; i--) {
			if (this.actions.get(i).getType() == BlackBox.ActionType.NextWave) {
				count++;
			}
		}
		return count;
	}

	public boolean loadLog(String logIn) {
		BufferedReader reader = new BufferedReader(new StringReader(logIn));
		return loadLog(reader);
	}

	public boolean loadLog(BufferedReader logIn) {
		System.out.println("BlackBox::loadLog: Loading log.");
		String line;
		int lineGameTime;
		int activateTick;
		BBAction newAction;
		this.actions.clear();

		try {
			while ((line = logIn.readLine()) != null) {
				Message message = MessageFactory.GetMessage(line);
				switch (message.getType()) {
					case Tick:
						break;
					case BuyCreep:
						MessageBuyCreep mbc = (MessageBuyCreep) message;
						lineGameTime = mbc.getTick();
						activateTick = lineGameTime + Context.actionDelay;
						newAction = new BBActionBuyCreeps(mbc.getTick(), mbc.getRow(), mbc.getCol(), mbc.getCreepType(), mbc.getCount(), mbc.getSize(), mbc.getPath(), mbc.getHealth(), activateTick, mbc.getSenderId());
						actions.add(newAction);
						break;
					case LoadMap:
						MessageLoadMap mlm = (MessageLoadMap) message;
						BBActionMapStart newMapStart = new BBActionMapStart(0, mlm.getMapLocation(), mlm.getMapMd5(), mlm.getVersion());
						context.loadMap(newMapStart.getMapId(), mlm.getMapMd5());
						actions.add(newMapStart);
						break;
					case SetMode:
						MessageSetMode msm = (MessageSetMode) message;
						lineGameTime = msm.getTick();
						activateTick = lineGameTime + Context.actionDelay;
						newAction = new BBActionSetMode(lineGameTime, msm.getRow(), msm.getCol(), msm.getMode(), msm.getValue(), activateTick, msm.getSenderId());
						actions.add(newAction);
						break;
					case TowerBuy:
						MessageTowerBuy mtb = (MessageTowerBuy) message;
						lineGameTime = mtb.getTick();
						activateTick = lineGameTime + Context.actionDelay;
						newAction = new BBActionNewTower(lineGameTime, mtb.getRow(), mtb.getCol(), mtb.getTowerType(), mtb.getSenderId(), activateTick);
						actions.add(newAction);
						break;
					case TowerUpgrade:
						MessageTowerUpgrade mtu = (MessageTowerUpgrade) message;
						lineGameTime = mtu.getTick();
						activateTick = lineGameTime + Context.actionDelay;
						newAction = new BBActionUpgradeTower(lineGameTime, mtu.getRow(), mtu.getCol(), activateTick, mtu.getSenderId());
						actions.add(newAction);
						break;
					case TowerSell:
						MessageTowerSell mts = (MessageTowerSell) message;
						lineGameTime = mts.getTick();
						activateTick = lineGameTime + Context.actionDelay;
						newAction = new BBActionSellTower(lineGameTime, mts.getRow(), mts.getCol(), activateTick, mts.getSenderId());
						actions.add(newAction);
						break;
					case GameEnd:
						MessageGameEnd mge = (MessageGameEnd) message;
						newAction = new BBActionEnd(mge.getTick(), mge.getSenderId());
						actions.add(newAction);
						break;
					case PlayerList:
						MessagePlayerList mpl = (MessagePlayerList) message;
						newAction = new BBActionPlayerList(mpl);
						actions.add(newAction);
						break;
					case Say:
						break;
					case PlayerForfeit:
						MessagePlayerForfeit mpf = (MessagePlayerForfeit) message;
						newAction = new BBActionPlayerForfeit(mpf.getTick(), mpf.getSenderId());
						actions.add(newAction);
					case Unknown:
						String[] parts;
						parts = line.trim().split(" ");
						try {
							lineGameTime = new Integer(parts[0]).intValue();
							activateTick = lineGameTime;
						} catch (NumberFormatException ex) {
							System.out.println("BlackBox::loadLog: Ignoring line: " + line);
							continue;
						}
						switch (parts[1].charAt(0)) {
							case 'W':
								newAction = new BBActionNextWave(lineGameTime);
								this.actions.add(newAction);
								break;
							case 'E':
								newAction = new BBActionEnd(lineGameTime, 0);
								this.actions.add(newAction);
								break;
							default:
								System.err.println("BlackBox::loadLog: Unknown action: " + parts[1]);
								break;
						}
						break;
					default:
						System.err.println("BlackBox::loadLog: Unhandled message type: " + message.getType());
						break;
				}
			}
		} catch (IOException ex) {
			ex.printStackTrace();
			return false;
		}
		this.fireActionCountChanged();
		return true;
	}

	public boolean checkBackLog() {
		if (backlog.size() > 0) {
			int checkTime = gameTime.getTime();
			boolean more = true;
			while (more) {
				GameMessage message = backlog.get(0);
				if (message.getTick() <= checkTime) {
					if (this.handleMessage(message)) {
						backlog.remove(0);
					} else {
						return false;
					}
				} else {
					more = false;
				}
				if (backlog.size() == 0) {
					more = false;
				}
			}
		}
		return true;
	}

	public int getBacklogSize() {
		return backlog.size();
	}

	public boolean readLine(GameMessage message) {
		synchronized (gameTime) {
			backlog.add(message);
			return true;
		}
	}

	public boolean handleMessage(GameMessage message) {
		int messageGameTime = message.getTick();
		BBAction newAction;
		int activateTick;
		switch (message.getType()) {
			case Tick:
				MessageTick mt = (MessageTick) message;
				if (context.getLocalPlayerId() == mt.getSenderId()) {
					// Tick by us?? rejoining running game?
					switch (context.getGameState()) {
						case mpCatchup:
							break;
						default:
							//System.err.println("BlackBox::readLine: Tick (" + mt.getTick() + ") send in our name. Problem rejoining?");
							break;
					}
				} else {
					// tick by another player
					ContextPlayer player = context.getPlayer(mt.getSenderId());
					if (player != null) {
						player.setLastTick(mt.getTick());
					} else {
						System.err.println("BlackBox::handleMessage: tick from non-existing player " + mt.getSenderId());
					}
				}
				break;
			case BuyCreep:
				MessageBuyCreep mbc = (MessageBuyCreep) message;
				activateTick = messageGameTime + Context.actionDelay;
				newAction = new BBActionBuyCreeps(messageGameTime, mbc.getRow(), mbc.getCol(), mbc.getCreepType(), mbc.getCount(), mbc.getSize(), mbc.getPath(), mbc.getHealth(), activateTick, mbc.getSenderId());
				newAction.execute(context);
				break;
			case LoadMap:
				MessageLoadMap mlm = (MessageLoadMap) message;
				if (!HexTD.VERSION.equals(mlm.getVersion())) {
					System.err.println("BlackBox::handleMessage: Warning: other side is using version" + mlm.getVersion() + "\n but this is version " + HexTD.VERSION + ",\n game may not work as expected!");
					context.showAlert("Warning: other side is using version" + mlm.getVersion() + "\n but this is version " + HexTD.VERSION + ",\n game may not work as expected!");
				}
				// TODO: This should handle delayed-maploading!

				Boolean retval = context.loadMap(mlm.getMapLocation(), mlm.getMapMd5());
				newAction = new BBActionMapStart(0, context.getMapLocation(), context.getMapInfo().getMapMD5(), HexTD.VERSION);
				this.actions.add(newAction);
				return retval;

			case SetMode:
				MessageSetMode msm = (MessageSetMode) message;
				activateTick = messageGameTime + Context.actionDelay;
				newAction = new BBActionSetMode(messageGameTime, msm.getRow(), msm.getCol(), msm.getMode(), msm.getValue(), activateTick, msm.getSenderId());
				newAction.execute(context);
				break;

			case TowerBuy:
				MessageTowerBuy mtb = (MessageTowerBuy) message;
				activateTick = messageGameTime + Context.actionDelay;
				newAction = new BBActionNewTower(messageGameTime, mtb.getRow(), mtb.getCol(), mtb.getTowerType(), mtb.getSenderId(), activateTick);
				newAction.execute(context);
				break;

			case TowerUpgrade:
				MessageTowerUpgrade mtu = (MessageTowerUpgrade) message;
				activateTick = messageGameTime + Context.actionDelay;
				newAction = new BBActionUpgradeTower(messageGameTime, mtu.getRow(), mtu.getCol(), activateTick, mtu.getSenderId());
				newAction.execute(context);
				break;
			case TowerSell:
				MessageTowerSell mts = (MessageTowerSell) message;
				activateTick = messageGameTime + Context.actionDelay;
				newAction = new BBActionSellTower(messageGameTime, mts.getRow(), mts.getCol(), activateTick, mts.getSenderId());
				newAction.execute(context);
				break;
			case PlayerForfeit:
				MessagePlayerForfeit mpf = (MessagePlayerForfeit) message;
				newAction = new BBActionPlayerForfeit(mpf.getTick(), mpf.getSenderId());
				newAction.execute(context);
				break;
			case Unknown:
				System.out.println("BlackBox::handleMessage: Unknown message");
				break;
			default:
				System.err.println("BlackBox::handleMessage: Unknown message type: " + message.getType());
				break;
		}
		return true;
	}
	/*
	 * Replay stuff
	 */

	public void doActions() {
		int gTime = gameTime.getTime();
		if (replayPointer < actions.size()) {
			BBAction nextAction = actions.get(replayPointer);
			while ((replayPointer < actions.size()) && (nextAction.getGameTime() <= gTime)) {
				if (nextAction.getGameTime() < gTime) {
					//System.out.println("BlackBox::doActions: Delta=" + (nextAction.getGameTime() - gameTime));
				}
				nextAction.execute(context);
				fireActionExecutedEvent(nextAction);
				replayPointer++;
				if (replayPointer < actions.size()) {
					nextAction = actions.get(replayPointer);
				}
			}
		}
	}

	public boolean newTower(int actionGameTime, int col, int row, TowerFactory.type t, int owner, int activateTick, boolean transmit) {
		boolean success = false;
		synchronized (gameTime) {
			if (actionGameTime == -1) {
				actionGameTime = gameTime.getTime();
				activateTick = actionGameTime + Context.actionDelay;
			}
			Hex hex = context.getGameGrid().getHex(col, row);
			if (hex.isBuildable(owner)) {
				// TODO: do we still need this check?
				// Only queue a payment if we are the player doing the building.
				if (owner == context.getLocalPlayerId() || transmit) {
					ContextPlayer player = context.getPlayer(owner);
					player.queuePayment(t.price);
				}
				// Place the tower
				//System.out.println("BlackBox::newTower: " + actionGameTime + "/" + activateTick + " User " + owner + " building new tower of type " + t.toString() + " at " + row + "," + col);
				Tower tempTower = TowerFactory.createTower(t, context, hex, owner, activateTick);
				context.addTower(tempTower);

				// Record it
				BBAction newAction = new BBActionNewTower(actionGameTime, row, col, t, owner, activateTick);
				actions.add(newAction);
				// Maybe send it to the other players
				if (transmit && context.getGameState().hasNetwork) {
					context.messageSend(newAction.getString());
				}
				success = true;
			} else {
				if (!transmit) {
					System.err.println("BlackBox::newTower: " + actionGameTime + " Hex " + row + "," + col + " not buildable for user " + owner + " for activation at " + activateTick);
				}
			}
		}
		this.fireActionCountChanged();
		return success;
	}

	public boolean sellTower(int actionGameTime, int col, int row, int activateTick, int owner, boolean transmit) {
		boolean success = false;
		synchronized (gameTime) {
			if (actionGameTime == -1) {
				actionGameTime = gameTime.getTime();
				activateTick = actionGameTime + Context.actionDelay;
			}
			Hex hex = this.context.getGameGrid().getHex(col, row);
			if (hex.hasTower()) {
				Tower t = hex.getTower();
				if (t.getOwner() != owner) {
					System.err.println("BlackBox::sellTower: WARNING: " + actionGameTime + "(" + actionGameTime + ") Player " + owner + " trying to sell tower at " + row + "," + col + " owned by " + t.getOwner());
					return false;
				}
				t.queueSell(activateTick);

				// Record it
				BBAction newAction = new BBActionSellTower(actionGameTime, row, col, activateTick, owner);
				this.actions.add(newAction);
				// Maybe send it to the other players
				if (transmit && context.getGameState().hasNetwork) {
					this.context.messageSend(newAction.getString());
				}
				success = true;
			}
		}
		this.fireActionCountChanged();
		return success;
	}

	public boolean upgradeTower(int actionGameTime, int col, int row, int activateTick, int owner, boolean transmit) {
		boolean success = false;
		synchronized (gameTime) {
			if (actionGameTime == -1) {
				actionGameTime = gameTime.getTime();
				activateTick = actionGameTime + Context.actionDelay;
			}
			Hex hex = context.getGameGrid().getHex(col, row);
			if (hex.hasTower()) {
				Tower t = hex.getTower();
				int tOwner = t.getOwner();
				if (tOwner != owner) {
					System.err.println("BlackBox::upgradeTower: WARNING: " + actionGameTime + "(" + activateTick + ") Player " + owner + " trying to upgrade tower at " + row + "," + col + " owned by " + t.getOwner());
					return false;
				}
				// TODO: do we still need this check?
				// Only queue a payment if we are the player doing the building.
				if (tOwner == context.getLocalPlayerId() || transmit) {
					ContextPlayer player = context.getPlayer(t.getOwner());
					player.queuePayment(t.getUpgradePrice(true));
				}
				t.queueUpgrade(activateTick);

				// Record it
				BBAction newAction = new BBActionUpgradeTower(actionGameTime, row, col, activateTick, owner);
				actions.add(newAction);
				// Maybe send it to the other players
				if (transmit && context.getGameState().hasNetwork) {
					context.messageSend(newAction.getString());
				}
				success = true;
			} else {
				System.err.println("BlackBox::upgradeTower: WARNING: " + actionGameTime + "(" + activateTick + ") Player " + owner + " trying to upgrade tower at " + row + "," + col + " but there is no tower there!");
			}
		}
		fireActionCountChanged();
		return success;
	}

	public boolean nextWave(int actionGameTime) {
		boolean success = false;
		synchronized (gameTime) {
			if (actionGameTime == -1) {
				actionGameTime = gameTime.getTime();
			}
			if (this.context.getEnemyCount() == 0) {
				System.out.println("BlackBox::nextWave: " + actionGameTime + "(" + actionGameTime + ") Sending next Wave.");
				this.context.nextWave();
				// Record it
				BBAction newAction = new BBActionNextWave(actionGameTime);
				actions.add(newAction);
				success = true;
			} else {
			}
		}
		this.fireActionCountChanged();
		return success;
	}

	public boolean setMode(int actionGameTime, int col, int row, int m, int v, int activateTick, int owner, boolean transmit) {
		boolean success = false;
		synchronized (gameTime) {
			if (actionGameTime == -1) {
				actionGameTime = gameTime.getTime();
				activateTick = actionGameTime + Context.actionDelay;
			}
			Hex hex = context.getGameGrid().getHex(col, row);
			if (hex.hasTower()) {
				Tower t = hex.getTower();
				if (t.getOwner() != owner) {
					System.err.println("BlackBox::setMode: WARNING: " + actionGameTime + "(" + actionGameTime + ") Player " + owner + " trying to set mode on tower at " + row + "," + col + " owned by " + t.getOwner());
					return false;
				}
				if (t.setMode(activateTick, m, v)) {
					// Record it
					BBAction newAction = new BBActionSetMode(actionGameTime, row, col, m, v, activateTick, owner);
					actions.add(newAction);
					// Maybe send it to the other players
					if (transmit && context.getGameState().hasNetwork) {
						context.messageSend(newAction.getString());
					}
					success = true;
				}
			}
		}
		this.fireActionCountChanged();
		return success;
	}

	public boolean buyCreeps(int actionGameTime, int col, int row, int type, int count, int size, int path, int health, int activateTick, int owner, boolean transmit) {
		boolean success = false;
		synchronized (gameTime) {
			if (actionGameTime == -1) {
				actionGameTime = gameTime.getTime();
				activateTick = actionGameTime + Context.actionDelay;
			}
			Hex hex = context.getGameGrid().getHex(col, row);
			if (hex.hasTower()) {
				Tower t = hex.getTower();
				int tOwner = t.getOwner();

				if (tOwner != owner) {
					System.err.println("BlackBox::buyCreeps: WARNING: " + actionGameTime + "(" + actionGameTime + ") Player " + owner + " trying to buy creeps in tower at " + row + "," + col + " owned by " + t.getOwner());
					return false;
				}
				if (t instanceof TowerHome) {
					TowerHome th = (TowerHome) t;
					if (th.queueSendCreeps(activateTick, type, count, size, path, health, tOwner == context.getLocalPlayerId())) {
						//System.out.println("BlackBox::buyEnemies: " + context.gameTime + " User " + t.getOwner() + " bought " + count + " type " + type + " size " + size + " health " + health + " at " + row + "," + col + " path " + path);
						// Record it
						BBAction newAction = new BBActionBuyCreeps(actionGameTime, row, col, type, count, size, path, health, activateTick, owner);
						actions.add(newAction);
						// Maybe send it to the other players
						if (transmit && context.getGameState().hasNetwork) {
							context.messageSend(newAction.getString());
						}
						success = true;
					} else {
						System.err.println("BlackBox::buyEnemies: " + gameTime + "(" + actionGameTime + ") User " + t.getOwner() + " can not queue enemies at " + row + "," + col);
					}
				}
			}
		}
		return success;
	}

	public boolean startMap(String map) {
		boolean success = false;
		synchronized (gameTime) {
			System.out.println("BlackBox::startMap: " + gameTime + " Starting map " + map);
			if (map.equalsIgnoreCase("Custom")) {
				System.out.println("Replay of custom map, cannot make sure it is loaded correctly during replay.");
				context.loadMap(context.loadedMapInfo, "");
				success = true;
			} else {
				success = context.loadMap(map, null);
				System.out.println("BlackBox::startMap: Starting map '" + map + "' done, success=" + success);
			}

			if (success) {
				// We're starting a map, thus clear this blackbox for recording
				clear();
				// Record it
				MapInfo loadedMap = context.getMapInfo();
				BBAction newAction = new BBActionMapStart(gameTime.getTime(), map, loadedMap.getMapMD5(), HexTD.VERSION);
				actions.add(newAction);
				context.setGameResult(HexTD.GameResult.active);
			}

		}
		fireActionCountChanged();
		return success;
	}

	public boolean playerForfeit(int gameTime, int player) {
		System.out.println("BlackBox::playerForfeit: Player " + player + " has quit.");
		ContextPlayer p = context.getPlayer(player);
		int lives = p.getLives();
		p.removeLife(lives);
		BBAction newAction = new BBActionPlayerForfeit(gameTime, player);
		actions.add(newAction);
		return true;
	}

	public boolean endGame(int actionGameTime, boolean transmit) {
		synchronized (gameTime) {
			if (actionGameTime == -1) {
				actionGameTime = gameTime.getTime();
			}
			System.out.println("BlackBox::endGame: " + gameTime + "(" + actionGameTime + ") Ending game");
			context.endGame();
			BBAction newAction = new BBActionEnd(actionGameTime, context.getLocalPlayerId());
			actions.add(newAction);
			// Maybe send it to the other players
			if (transmit && context.getGameState().hasNetwork) {
				context.messageSend(newAction.getString());
			}
		}
		fireActionCountChanged();
		return true;
	}

	public boolean loadPlayerList(List<MPPlayerInfo> players) {
		for (MPPlayerInfo info : players) {
			ContextPlayer p = context.getPlayer(info.gameId);
			if (p != null) {
				p.setPlayerName(info.name);
				p.setUserId(info.userId);
			}
		}
		return true;
	}
	/*
	 * Listeners stuff 
	 */

	public void addBBActionListener(BBActionListener l) {
		this.actionListeners.add(l);
	}

	public void removeBBActionListener(BBActionListener l) {
		this.actionListeners.remove(l);
	}

	private void fireActionExecutedEvent(BBAction a) {
		for (int i = 0; i < this.actionListeners.size(); i++) {
			this.actionListeners.get(i).ActionExecuted(a);
		}
	}

	private void fireActionCountChanged() {
		for (int i = 0; i < this.actionListeners.size(); i++) {
			this.actionListeners.get(i).ActionCountChanged();
		}
	}
}
